/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package bgu.ppm.dat.model.impl;

import bgu.ppm.dat.model.AggregatedField;
import bgu.ppm.dat.model.Field;
import bgu.ppm.dat.model.FilterField;
import bgu.ppm.dat.model.Pivot;
import bgu.ppm.dat.model.Table;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

/**
 *
 * @author User
 */
public class InMemoryPivotTable<T> implements Table {

    private Object[][] columns;
    private Object[][] rows;
    private Object[][] data;

    public InMemoryPivotTable(Pivot<T> pivot) {
        generateData(pivot);
    }

    @Override
    public Object[][] getColumnHeaders() {
        return columns;
    }

    @Override
    public Object[][] getRowHeaders() {
        return rows;
    }

    @Override
    public Object getCell(int column, int row) {
        return data[column][row];
    }

    private void generateData(Pivot<T> pivot) {
        Map<Field, Set<Object>> axis = new HashMap<>();

        for (T r : pivot.getDataRecords()) {
            if (!isRestrictedRecord(pivot, r)) {
                extractFieldsData(pivot.getSelectedSeriesFields(), r, axis);
                extractFieldsData(pivot.getSelectedAxisFields(), r, axis);
            }
        }

        columns = permutateFieldsValues(pivot.getSelectedSeriesFields(), axis, pivot.getSelectedValuesFields());
        rows = permutateFieldsValues(pivot.getSelectedAxisFields(), axis, Collections.EMPTY_LIST);

        Arrays.sort(columns, new Comparator<Object[]>() {
            @Override
            public int compare(Object[] o1, Object[] o2) {
                return Arrays.toString(o1).compareTo(Arrays.toString(o2));
            }
        });

        Arrays.sort(rows, new Comparator<Object[]>() {
            @Override
            public int compare(Object[] o1, Object[] o2) {
                return Arrays.toString(o1).compareTo(Arrays.toString(o2));
            }
        });

        Map<ObjectArray, Integer> lookupColumns = generateLookup(columns);
        Map<ObjectArray, Integer> lookupRows = generateLookup(rows);

        Map<Integer, Collection<Object>> values = generateValues(pivot, lookupColumns, lookupRows);

        fillValues(pivot, values, lookupColumns, lookupRows);
    }

    private void extractFieldsData(Collection<? extends Field> selectedFields, T r, Map<Field, Set<Object>> axis) {
        for (Field<?, T> f : selectedFields) {
            if (!axis.containsKey(f)) {
                axis.put(f, new HashSet<>());
            }
            axis.get(f).add(f.getValue(r));
        }
    }

    private Object[][] permutateFieldsValues(Collection<? extends Field> selectedFields, Map<Field, Set<Object>> axis, Collection add) {
        Map<Field, Object[]> oAxis = new HashMap<>();
        Object[] oAdd = add.toArray();

        for (Field f : selectedFields) {
            if (!axis.containsKey(f)) {
                axis.put(f, new HashSet<>());
            }
            oAxis.put(f, axis.get(f).toArray());
        }

        int amountOfValues = 1, t = 0;
        int[] factors = new int[selectedFields.size() + (oAdd.length == 0 ? 0 : 1)];

        for (Field f : selectedFields) {
            factors[t++] = amountOfValues;
            amountOfValues *= oAxis.get(f).length;
        }

        if (oAdd.length != 0) {
            factors[t] = amountOfValues;
            amountOfValues *= oAdd.length;
        }

        Object[][] result = new Object[amountOfValues][];

        for (int i = 0; i < result.length; i++) {
            result[i] = new Object[factors.length];
            int j = 0;
            for (Field f : selectedFields) {
                result[i][j] = oAxis.get(f)[(i / factors[j]) % oAxis.get(f).length];
                j++;
            }

            if (oAdd.length != 0) {
                result[i][j] = oAdd[(i / factors[j]) % oAdd.length];
            }
        }

        return result;
    }

    private Map<ObjectArray, Integer> generateLookup(Object[][] columns) {
        Map<ObjectArray, Integer> res = new HashMap<>();

        for (int i = 0; i < columns.length; i++) {
            res.put(new ObjectArray(columns[i]), i);
        }

        return res;
    }

    private Map<Integer, Collection<Object>> generateValues(Pivot<T> pivot, Map<ObjectArray, Integer> lookupColumns, Map<ObjectArray, Integer> lookupRows) {
        Map<Integer, Collection<Object>> res = new HashMap<>();

        for (T r : pivot.getDataRecords()) {
            if (isRestrictedRecord(pivot, r)) {
                continue;
            }

            for (AggregatedField v : pivot.getSelectedValuesFields()) {
                int column = getColumnIndex(r, pivot.getSelectedSeriesFields(), v, lookupColumns);
                int row = getRowIndex(r, pivot.getSelectedAxisFields(), lookupRows);
                int index = column * rows.length + row;

                if (!res.containsKey(index)) {
                    res.put(index, new LinkedList<>());
                }

                res.get(index).add(v.getValue(r));
            }
        }

        return res;
    }

    private int getColumnIndex(T r, Collection<Field> selectedFields, AggregatedField v, Map<ObjectArray, Integer> lookup) {
        Object[] res = new Object[selectedFields.size() + 1];
        int i = 0;

        for (Field f : selectedFields) {
            res[i++] = f.getValue(r);
        }

        res[i] = v;

        return lookup.get(new ObjectArray(res));
    }

    private int getRowIndex(T r, Collection<Field> selectedFields, Map<ObjectArray, Integer> lookup) {
        Object[] res = new Object[selectedFields.size()];
        int i = 0;

        for (Field f : selectedFields) {
            res[i++] = f.getValue(r);
        }

        return lookup.get(new ObjectArray(res));
    }

    private void fillValues(Pivot<T> pivot, Map<Integer, Collection<Object>> values, Map<ObjectArray, Integer> lookupColumns, Map<ObjectArray, Integer> lookupRows) {
        data = new Object[columns.length][];

        for (int i = 0; i < columns.length; i++) {
            data[i] = new Object[rows.length];

            for (int j = 0; j < rows.length; j++) {
                int column = lookupColumns.get(new ObjectArray(columns[i]));
                int row = lookupRows.get(new ObjectArray(rows[j]));
                int index = column * rows.length + row;
                final Collection<Object> value = values.get(index);

                if (value == null) {
                    data[i][j] = null;
                } else {
                    data[i][j] = ((AggregatedField) columns[i][columns[i].length - 1]).getAggregationFunction().aggregate(value);
                }
            }
        }
    }

    @Override
    public String toString() {
        return "Columns: \n" + joinI(columns) + "\n" + "Rows: \n" + joinI(rows) + "Table: \n" + joinI(data) + "\n";
    }

    public String joinI(Object[][] c) {
        LinkedList<String> subRes = new LinkedList<>();

        for (Object[] i : c) {
            subRes.addLast(join(i) + ",\n");
        }

        return join(subRes);
    }

    public String join(Object[] c) {
        StringBuilder sb = new StringBuilder("{");
        for (Object s : c) {
            sb.append("" + s).append(", ");
        }
        if (sb.length() != 1) {
            sb.delete(sb.length() - 2, sb.length());
        }

        return sb.append("}").toString();
    }

    public String join(Iterable c) {
        StringBuilder sb = new StringBuilder("{");
        for (Object s : c) {
            sb.append("" + s).append(", ");
        }
        if (sb.length() != 1) {
            sb.delete(sb.length() - 2, sb.length());
        }

        return sb.append("}").toString();
    }

    private boolean isRestrictedRecord(Pivot<T> pivot, T r) {
        for (FilterField ff : pivot.getSelectedFilterFields()) {
            if (ff.getRestrictedValues().contains(ff.getValue(r))) {
                return true;
            }
        }
        return false;
    }

    private class ObjectArray {

        Object[] array;

        public ObjectArray(Object[] array) {
            this.array = array;
        }

        public Object[] getArray() {
            return array;
        }

        public void setArray(Object[] array) {
            this.array = array;
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 79 * hash + Arrays.deepHashCode(this.array);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final ObjectArray other = (ObjectArray) obj;
            if (!Arrays.deepEquals(this.array, other.array)) {
                return false;
            }
            return true;
        }
    }
}
